package cn.edu.dgut.css.sai.dgutoauth2.oauth;

import cn.edu.dgut.css.sai.dgutoauth2.oauth.SaiDGUTOAuth2UserService;
import cn.edu.dgut.css.sai.dgutoauth2.oauth.SaiDGUTAppLoginFilter;
import cn.edu.dgut.css.sai.dgutoauth2.oauth.SaiDGUTAuthorizationResponseFilter;
import cn.edu.dgut.css.sai.dgutoauth2.oauth.SaiDGUTOAuth2AccessTokenResponseClient;
import cn.edu.dgut.css.sai.dgutoauth2.oauth.SaiDGUTOAuth2AuthorizationRequestResolver;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.http.HttpMessageConverters;
import org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration;
import org.springframework.boot.autoconfigure.security.StaticResourceLocation;
import org.springframework.boot.autoconfigure.security.servlet.PathRequest;
import org.springframework.boot.autoconfigure.web.client.RestTemplateAutoConfiguration;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.boot.web.client.RestTemplateCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.client.OkHttp3ClientHttpRequestFactory;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.security.config.annotation.ObjectPostProcessor;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.configuration.ObjectPostProcessorConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfiguration;
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter;
import org.springframework.security.oauth2.client.web.OAuth2LoginAuthenticationFilter;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.web.client.RestTemplate;

import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

/**
 * 应用程序Spring Security配置类
 *
 * @author sai
 * @since 1.0
 */
@Configuration(proxyBeanMethods = false)
public class SaiDGUTOAuth2LoginConfiguration {

    /**
     * 自定义一个 MappingJackson2HttpMessageConverter
     *
     * <li>莞工CAS的API响应结果都是text/html,默认的mappingJackson2HttpMessageConverter不支持这个Content-Type，所以这里添加一个自定义的。</li>
     * <li>默认配置的MappingJackson2HttpMessageConverter不支持text/plain的content-type响应的转换。</li>
     * <li>默认定义的{@link MappingJackson2HttpMessageConverter#getDefaultCharset()}的默认字符集为null，这会导致生成响应请求的头部不会添加chartset=utf-8</li>
     * <li>部分浏览器如safari收到application/json的content-type响应时，如果json字符串包含中文字符会乱码显示。</li>
     * <li>这里声明一个HttpMessageConverter，实例是MappingJackson2HttpMessageConverter，并且配置它的默认字符集为StandardCharsets.UTF_8，当生成响应时，会在原响应content-type后加chartset=UTF-8</li>
     * <li>这个方法的返回值必须是HttpMessageConverter，否则不能注入并配置mvc的{@link HttpMessageConverter},可能是{@link ObjectProvider}的原因。</li>
     * <li>chrome浏览器不受影响。</li>
     * <li>自动配置类{@link HttpMessageConvertersAutoConfiguration}会把所有{@link HttpMessageConverter}的Bean封装到{@link HttpMessageConverters}</li>
     * <li>自动配置类{@link RestTemplateAutoConfiguration}会从spring容器获取{@link HttpMessageConverters}配置全局{@link RestTemplateBuilder}</li>
     * <li>这里自定义的{@link MappingJackson2HttpMessageConverter}最终会配置到全局{@link RestTemplateBuilder}</li>
     *
     * @see HttpMessageConvertersAutoConfiguration
     * @see RestTemplateAutoConfiguration
     */
    @Bean
    HttpMessageConverter<Object> customHttpMessageConverter() {
        MappingJackson2HttpMessageConverter httpMessageConverter = new MappingJackson2HttpMessageConverter();
        List<MediaType> mediaTypes = new ArrayList<>(Collections.singletonList(MediaType.TEXT_HTML));
        httpMessageConverter.setSupportedMediaTypes(mediaTypes);
        httpMessageConverter.setDefaultCharset(StandardCharsets.UTF_8);
        return httpMessageConverter;
    }

    /**
     * 配置全局{@link RestTemplateBuilder},改用{@link OkHttp3ClientHttpRequestFactory},参考{@link RestTemplate#setRequestFactory(ClientHttpRequestFactory)}.
     * <p>{@link RestTemplate}默认使用{@link SimpleClientHttpRequestFactory},它是基于JDK自带的HTTP libraries ({@link java.net.HttpURLConnection}).</p>
     * <p>OkHttp库性能更优越，需要在maven工程pom文件中引入OkHttp依赖</p>
     * <p>自动配置类{@link RestTemplateAutoConfiguration#restTemplateBuilderConfigurer}会从spring容器获取{@link RestTemplateCustomizer}配置全局{@link RestTemplateBuilder}</p>
     *
     * @return {@link RestTemplateCustomizer}
     * @see RestTemplateAutoConfiguration#restTemplateBuilderConfigurer
     * @see RestTemplateBuilder
     * @see RestTemplate
     * @see RestTemplateCustomizer
     */
    @Bean
    RestTemplateCustomizer restTemplateCustomizer() {
        return restTemplate -> restTemplate.setRequestFactory(new OkHttp3ClientHttpRequestFactory());
    }

    /**
     * 定义Spring Security安全过滤链
     *
     * @see EnableWebSecurity
     * @see WebSecurityConfiguration#setFilterChainProxySecurityConfigurer(ObjectPostProcessor, List)
     * @see org.springframework.security.config.annotation.web.configuration.HttpSecurityConfiguration
     * @see AuthenticationConfiguration
     * @see ObjectPostProcessorConfiguration
     */
    @SuppressWarnings("JavadocReference")
    @Bean
    SecurityFilterChain DGUTAppSecurityFilterChain(HttpSecurity http,
                                                   ObjectPostProcessor<Object> postProcessor,
                                                   RestTemplateBuilder restTemplateBuilder,
                                                   ClientRegistrationRepository clientRegistrationRepository) throws Exception {
        // @formatter:off
        http.authorizeRequests()
                .anyRequest().authenticated();

        // 添加一个filter,用于识别是否从i莞工App进入。
        http.addFilterBefore(postProcessor.postProcess(new SaiDGUTAppLoginFilter(clientRegistrationRepository)), OAuth2AuthorizationRequestRedirectFilter.class);

        // 添加一个filter,过滤dgut登录时的token参数改名为code
        http.addFilterBefore(postProcessor.postProcess(new SaiDGUTAuthorizationResponseFilter()), OAuth2LoginAuthenticationFilter.class);

        http.oauth2Login()
                // 定义处理莞工CAS回调接口的path,默认是/login/oauth2/code/* ,根据自已在申请莞工CAS时填报的回调地址修改。
                .loginProcessingUrl("/login/dgut")
                .tokenEndpoint()
                    .accessTokenResponseClient(new SaiDGUTOAuth2AccessTokenResponseClient(restTemplateBuilder)).and()
                .authorizationEndpoint()
                    .authorizationRequestResolver(new SaiDGUTOAuth2AuthorizationRequestResolver(clientRegistrationRepository)).and()
                .userInfoEndpoint()
                    .userService(new SaiDGUTOAuth2UserService(restTemplateBuilder))
        ;
        // @formatter:on

        return http.build();
    }

    /**
     * {@link WebSecurity}的配置器
     *
     * <li>配置忽略静态资源的part。</li>
     *
     * @see EnableWebSecurity
     * @see WebSecurityConfiguration#setFilterChainProxySecurityConfigurer(ObjectPostProcessor, List)
     * @see StaticResourceLocation
     */
    @Bean
    WebSecurityCustomizer DGUTWebSecurityCustomizer() {
        return webSecurity -> {
            // 不能在这个方法里忽略登录login的路径
            // 否则security的filter都不匹配login路径造成不能登录
            // 在这里忽略掉静态资源的路径是比较好的选择
            // 这与在配置文件设置security.ignoring是一样的
            //webSecurity.ignoring().regexMatchers("/index");
            webSecurity.ignoring().requestMatchers(PathRequest.toStaticResources().atCommonLocations());
        };
    }

}
